﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace AdvancedTracing
{
    // VERY IMPORTANT!!!
    // --------------------------------------------------
    // Dont write any debugging statements anywhere in this
    // class such as Debug.WriteLine or Trace.WriteLine
    // simply because it would go into an endless loop
    // --------------------------------------------------

    sealed public class DBTraceListener : TraceListener
    {
        #region Constants

        private const string STORED_PROC_NAME = "prc_writetraceentry";
        private const string STORED_PROC_MESSAGE_PARAM_NAME = "@message";
        private const string STORED_PROC_MESSAGETYPE_PARAM_NAME = "@type";
        private const string TRACE_SWITCH_NAME = "DBTraceSwitch";
        private const string TRACE_SWITCH_DESCRIPTION = "Trace switch defined in config file for configuring trace output to database";

        // Not defining it as readonly string so that in future it could come
        // from an external source and we can provide initializer for it
        private static readonly string DEFAULT_TRACE_TYPE = "Verbose";

        #endregion

        #region Class Data

        // Connection string for database
        private string _connectionString;

        // Trace Switch object for controlling trace output, defaulting to Verbose
        private TraceSwitch TraceSwitch = new TraceSwitch(TRACE_SWITCH_NAME, TRACE_SWITCH_DESCRIPTION, DEFAULT_TRACE_TYPE);

        // Lock object
        private object _fileLockObject = new object();

        #endregion

        #region Properties

        public override bool IsThreadSafe
        {
            get { return true; }
        }

        public string ConnectionString
        {
            get
            {
                if ( string.IsNullOrEmpty(this._connectionString))
                {
                    this.LoadAttributes();
                }
                return this._connectionString;
            }
            private set { this._connectionString = value; }
        }

        #endregion

        #region Methods

        async private void WriteEntryToInternalLog(string msg)
        {
            await Task.Run(() =>
                {
                    lock (this._fileLockObject)
                    {
                        try
                        {
                            File.AppendAllText(AppDomain.CurrentDomain.BaseDirectory + @"\DBTraceListener.log",
                                string.Format("{0}{1}: {2}", Environment.NewLine, DateTime.Now.ToString(), msg));
                        }
                        catch { }
                    }
                });
        }

        internal bool ShouldLogTrace(TraceEventType eventType)
        {
            bool shouldLog = true;

            switch (eventType)
            {
                case TraceEventType.Critical:
                case TraceEventType.Error:
                    shouldLog = this.TraceSwitch.TraceError;
                    break;

                case TraceEventType.Warning:
                    shouldLog = this.TraceSwitch.TraceWarning;
                    break;

                case TraceEventType.Information:
                    shouldLog = this.TraceSwitch.TraceInfo;
                    break;

                case TraceEventType.Start:
                case TraceEventType.Stop:
                case TraceEventType.Suspend:
                case TraceEventType.Resume:
                case TraceEventType.Transfer:
                case TraceEventType.Verbose:
                    shouldLog = this.TraceSwitch.TraceVerbose;
                    break;
            }

            return shouldLog;
        }

        #region TraceEvent

        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id)
        {
            this.TraceEvent(eventCache, source, eventType, id, string.Empty);
        }

        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
        {
            XElement msg;

            try
            {
                if (!this.ShouldLogTrace(eventType))
                    return;

                if (eventType == TraceEventType.Critical ||
                        eventType == TraceEventType.Error ||
                        eventType == TraceEventType.Warning)
                {
                    msg = new XElement("TraceLog",
                                            new XElement("Message", message),
                                            new XElement("Id", id),
                                            new XElement("CallStack", eventCache.Callstack.ToString()),
                                            new XElement("ThreadId", eventCache.ThreadId),
                                            new XElement("ProcessId", eventCache.ProcessId)
                                       );
                }
                else
                {
                    msg = new XElement("TraceLog",
                                            new XElement("Message", message));
                }

                this.WriteLineInternalAsync(msg.ToString(), eventType.ToString());
            }
            catch(Exception ex)
            {
                this.WriteEntryToInternalLog(string.Format("Trace.TraceEvent failed with following exception: {0}", ex.ToString()));
            }
        }

        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
        {
            XElement msg;

            try
            {
                if (!this.ShouldLogTrace(eventType))
                    return;

                if (eventType == TraceEventType.Critical ||
                        eventType == TraceEventType.Error ||
                        eventType == TraceEventType.Warning)
                {
                    msg = new XElement("TraceLog",
                                            new XElement("Message", string.Format(format, args)),
                                            new XElement("Id", id),
                                            new XElement("CallStack", eventCache.Callstack.ToString()),
                                            new XElement("ThreadId", eventCache.ThreadId),
                                            new XElement("ProcessId", eventCache.ProcessId)
                                       );
                }
                else
                {
                    msg = new XElement("TraceLog",
                                            new XElement("Message", string.Format(format, args)));
                }

                this.WriteLineInternalAsync(msg.ToString(), eventType.ToString());
            }
            catch (Exception ex)
            {
                this.WriteEntryToInternalLog(string.Format("Trace.TraceEvent failed with following exception: {0}", ex.ToString()));
            }
        }

        #endregion

        #region TraceTransfer

        public override void TraceTransfer(TraceEventCache eventCache, string source, int id, string message, Guid relatedActivityId)
        {
            try
            {
                if (this.ShouldLogTrace(TraceEventType.Transfer))
                {
                    XElement msg = new XElement("TraceLog",
                            new XElement("Message", message),
                            new XElement("Source", source),
                            new XElement("Id", id),
                            new XElement("RelatedActivityId", relatedActivityId.ToString()),
                            new XElement("CallStack", eventCache.Callstack.ToString()),
                            new XElement("ThreadId", eventCache.ThreadId),
                            new XElement("ProcessId", eventCache.ProcessId));

                    this.WriteLine(msg.ToString(), TraceEventType.Verbose.ToString());
                }
            }
            catch (Exception ex)
            {
                this.WriteEntryToInternalLog(string.Format("Trace.TraceTransfer failed with following exception: {0}", ex.ToString()));
            }
        }

        #endregion

        public override string ToString()
        {
            return string.Format("DBTraceListener using ConnectionString: {0}", this.ConnectionString);
        }

        #region Write Methods

        public override void Write(object o)
        {
            if (o != null)
            {
                this.WriteLine(o.ToString(), null);
            }
        }

        public override void Write(string message)
        {
            this.WriteLine(message, null);
        }

        public override void Write(object o, string category)
        {
            if (o != null)
            {
                this.WriteLine(o.ToString(), category);
            }
        }

        public override void Write(string message, string category)
        {
            this.WriteLine(message, category);
        }

        #endregion

        #region WriteLine Methods

        public override void WriteLine(object o)
        {
            if (o != null)
            {
                this.WriteLine(o.ToString(), null);
            }
        }

        public override void WriteLine(object o, string category)
        {
            if (o != null)
            {
                this.WriteLine(o.ToString(), category);
            }
        }

        public override void WriteLine(string message)
        {
            this.WriteLine(message, null);
        }

        override public void WriteLine(string message, string category)
        {
            try
            {
                if (!this.ShouldLogTrace(TraceEventType.Verbose))
                    return;

                // IMPORTANT!!!!
                // DO NOT WRITE ANY Debug.WriteLine or Trace.WriteLine statements in this method
                XElement msg = new XElement("TraceLog",
                                   new XElement("Message", message));

                this.WriteLineInternalAsync(msg.ToString(), category);
            }
            catch (Exception ex)
            {
                this.WriteEntryToInternalLog(string.Format("WriteLine failed with following exception: {0}", ex.ToString()));
            }
        }

        #endregion

        async private Task WriteLineInternalAsync(string message, string category)
        {
            // IMPORTANT!!!!
            // DO NOT WRITE ANY Debug.WriteLine or Trace.WriteLine statements in this method
            SqlConnection cn = null;
            SqlCommand cm = null;

            try
            {
                cn = new SqlConnection(this.ConnectionString);
                await cn.OpenAsync();

                cm = cn.CreateCommand();
                cm.CommandText = STORED_PROC_NAME;
                cm.CommandType = CommandType.StoredProcedure;
                cm.Parameters.Add(new SqlParameter(STORED_PROC_MESSAGE_PARAM_NAME, DBNull.Value));
                cm.Parameters.Add(new SqlParameter(STORED_PROC_MESSAGETYPE_PARAM_NAME, DBNull.Value));

                // save trace message to database
                cm.Parameters[STORED_PROC_MESSAGE_PARAM_NAME].Value = message;
                cm.Parameters[STORED_PROC_MESSAGETYPE_PARAM_NAME].Value = category;

                // execute command
                await cm.ExecuteNonQueryAsync();

                // close database connection
                cn.Close();
            }
            catch (Exception ex)
            {
                // Write entry to internal log file
                this.WriteEntryToInternalLog(ex.ToString());
            }
            finally
            {
                if (cn != null)
                    cn.Dispose(); cn = null;

                if (cm != null)
                    cm.Dispose(); cm = null;
            }
        }

        protected override string[] GetSupportedAttributes()
        {
            return new string[] { "connectionString" };
        }

        private void LoadAttributes()
        {
            if (Attributes.ContainsKey("connectionString"))
            {
                this.ConnectionString = this.Attributes["connectionString"];
            }
        }

        #endregion

        public DBTraceListener() : this(string.Empty) { }

        public DBTraceListener(string initializeData)
            : base(initializeData)
        {
            // Initialize connection object
            var cn = new SqlConnection();
            cn.ConnectionString = initializeData;

            // Also set connection string for the current instance
            this.ConnectionString = initializeData;

            try
            {
                cn.Open();
                cn.Close();
            }
            catch (Exception ex)
            {
                // Write to internal 
                this.WriteEntryToInternalLog(string.Format("Could not connect to database from the provided connection string. Exception: {0}", ex.ToString()));

                // Let the caller know that this listener object cannot do its 
                // work because it cannot establish connection to database
                //
                // Since Tracing framework is initialized by CLR, you would 
                // in all likelihood get Could not create type... error
                throw new CouldNotConnectToDBException(ex.Message);
            }
            finally
            {
                cn.Dispose(); cn = null;
            }
        }
    }
}